// Copyright (C) 2010, 2011, 2012 GlavSoft LLC.
// All rights reserved.
//
//-------------------------------------------------------------------------
// This file is part of the TightVNC software.  Please visit our Web site:
//
//                       http://www.tightvnc.com/
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//-------------------------------------------------------------------------
//
package com.glavsoft.viewer;

import com.glavsoft.viewer.swing.Surface;
import com.glavsoft.viewer.swing.UiSettings;
import com.glavsoft.viewer.swing.Utils;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

public class ContainerManager {

    public static final int FS_SCROLLING_ACTIVE_BORDER = 20;
    private final Viewer viewer;
    private JToggleButton zoomFitButton;
    private JToggleButton zoomFullScreenButton;
    private JButton zoomInButton;
    private JButton zoomOutButton;
    private JButton zoomAsIsButton;
    private JPanel outerPanel;
    private JScrollPane scroller;
    private Container container;
    private boolean forceResizable = true;
    private ButtonsBar buttonsBar;
    private Surface surface;
    private boolean isSeparateFrame;
    private Rectangle oldContainerBounds;
    private volatile boolean isFullScreen;
    private Border oldScrollerBorder;
    private JLayeredPane lpane;
    private EmptyButtonsBarMouseAdapter buttonsBarMouseAdapter;

    public ContainerManager(Viewer viewer) {
        this.viewer = viewer;
    }

    public Container createContainer(final Surface surface, boolean isSeparateFrame, boolean isApplet) {
        this.surface = surface;
        this.isSeparateFrame = isSeparateFrame;
        outerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)) {

            @Override
            public Dimension getSize() {
                return surface.getPreferredSize();
            }

            @Override
            public Dimension getPreferredSize() {
                return surface.getPreferredSize();
            }
        };
        lpane = new JLayeredPane() {

            @Override
            public Dimension getSize() {
                return surface.getPreferredSize();
            }

            @Override
            public Dimension getPreferredSize() {
                return surface.getPreferredSize();
            }
        };
        lpane.setPreferredSize(surface.getPreferredSize());
        lpane.add(surface, JLayeredPane.DEFAULT_LAYER, 0);
        outerPanel.add(lpane);

        JFrame frame;
        scroller = new JScrollPane(outerPanel);
        if (isSeparateFrame) {
            frame = new JFrame();
            if (!isApplet) {
                frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            }
            java.util.List<Image> icons = Utils.getIcons();
            if (icons.size() != 0) {
                frame.setIconImages(icons);
            }
            container = frame;
        } else {
            container = viewer;
        }
        container.setLayout(new BorderLayout(0, 0));
        container.add(scroller, BorderLayout.CENTER);

        if (isSeparateFrame) {
//			frame.pack();
            outerPanel.setSize(surface.getPreferredSize());
            internalPack(null);
            container.setVisible(true);
        }
        container.validate();
        return container;
    }

    public void pack() {
        final Dimension outerPanelOldSize = outerPanel.getSize();
        outerPanel.setSize(viewer.getSurface().getPreferredSize());
        if (container != viewer && !viewer.isZoomToFitSelected()) {
            internalPack(outerPanelOldSize);
        }
        updateZoomButtonsState();
        viewer.updateFrameTitle();
    }

    private void internalPack(Dimension outerPanelOldSize) {
        final Rectangle workareaRectangle = getWorkareaRectangle();
        if (workareaRectangle.equals(container.getBounds())) {
            forceResizable = true;
        }
        final boolean isHScrollBar = scroller.getHorizontalScrollBar().isShowing() && !forceResizable;
        final boolean isVScrollBar = scroller.getVerticalScrollBar().isShowing() && !forceResizable;

        boolean isWidthChangeable = true;
        boolean isHeightChangeable = true;
        if (outerPanelOldSize != null && viewer.getSurface().oldSize != null) {
            isWidthChangeable = forceResizable
                    || (outerPanelOldSize.width == viewer.getSurface().oldSize.width && !isHScrollBar);
            isHeightChangeable = forceResizable
                    || (outerPanelOldSize.height == viewer.getSurface().oldSize.height && !isVScrollBar);
        }
        forceResizable = false;
        container.validate();

        final Insets containerInsets = container.getInsets();
        Dimension preferredSize = container.getPreferredSize();
        Rectangle preferredRectangle = new Rectangle(container.getLocation(), preferredSize);

        if (null == outerPanelOldSize && workareaRectangle.contains(preferredRectangle)) {
            ((JFrame) container).pack();
        } else {
            Dimension minDimension = new Dimension(
                    containerInsets.left + containerInsets.right, containerInsets.top + containerInsets.bottom);
            if (buttonsBar != null && buttonsBar.isVisible) {
                minDimension.width += buttonsBar.getWidth();
                minDimension.height += buttonsBar.getHeight();
            }
            Dimension dim = new Dimension(preferredSize);
            Point location = container.getLocation();
            if (!isWidthChangeable) {
                dim.width = container.getWidth();
            } else {
                if (isVScrollBar) {
                    dim.width += scroller.getVerticalScrollBar().getWidth();
                }
                if (dim.width < minDimension.width) {
                    dim.width = minDimension.width;
                }

                int dx = location.x - workareaRectangle.x;
                if (dx < 0) {
                    dx = 0;
                    location.x = workareaRectangle.x;
                }
                int w = workareaRectangle.width - dx;
                if (w < dim.width) {
                    int dw = dim.width - w;
                    if (dw < dx) {
                        location.x -= dw;
                    } else {
                        dim.width = workareaRectangle.width;
                        location.x = workareaRectangle.x;
                    }
                }
            }
            if (!isHeightChangeable) {
                dim.height = container.getHeight();
            } else {

                if (isHScrollBar) {
                    dim.height += scroller.getHorizontalScrollBar().getHeight();
                }
                if (dim.height < minDimension.height) {
                    dim.height = minDimension.height;
                }

                int dy = location.y - workareaRectangle.y;
                if (dy < 0) {
                    dy = 0;
                    location.y = workareaRectangle.y;
                }
                int h = workareaRectangle.height - dy;
                if (h < dim.height) {
                    int dh = dim.height - h;
                    if (dh < dy) {
                        location.y -= dh;
                    } else {
                        dim.height = workareaRectangle.height;
                        location.y = workareaRectangle.y;
                    }
                }
            }
            if (!location.equals(container.getLocation())) {
                container.setLocation(location);
            }
            if (!isFullScreen) {
                container.setSize(dim);
            }
        }
        scroller.revalidate();
    }

    private Rectangle getWorkareaRectangle() {
        final GraphicsConfiguration graphicsConfiguration = container.getGraphicsConfiguration();
        final Rectangle screenBounds = graphicsConfiguration.getBounds();
        final Insets screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(graphicsConfiguration);

        screenBounds.x += screenInsets.left;
        screenBounds.y += screenInsets.top;
        screenBounds.width -= screenInsets.left + screenInsets.right;
        screenBounds.height -= screenInsets.top + screenInsets.bottom;
        return screenBounds;
    }

    void addZoomButtons() {
        buttonsBar.createStrut();
        zoomOutButton = buttonsBar.createButton("zoom-out", "Zoom Out", new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                zoomFitButton.setSelected(false);
                viewer.getUiSettings().zoomOut();
            }
        });
        zoomInButton = buttonsBar.createButton("zoom-in", "Zoom In", new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                zoomFitButton.setSelected(false);
                viewer.getUiSettings().zoomIn();
            }
        });
        zoomAsIsButton = buttonsBar.createButton("zoom-100", "Zoom 100%", new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                zoomFitButton.setSelected(false);
                forceResizable = false;
                viewer.getUiSettings().zoomAsIs();
            }
        });

        zoomFitButton = buttonsBar.createToggleButton("zoom-fit", "Zoom to Fit Window",
                new ItemListener() {

                    @Override
                    public void itemStateChanged(ItemEvent e) {
                        if (e.getStateChange() == ItemEvent.SELECTED) {
                            viewer.setZoomToFitSelected(true);
                            forceResizable = true;
                            zoomToFit();
                            updateZoomButtonsState();
                        } else {
                            viewer.setZoomToFitSelected(false);
                        }
                        viewer.setSurfaceToHandleKbdFocus();
                    }
                });

        zoomFullScreenButton = buttonsBar.createToggleButton("zoom-fullscreen", "Full Screen",
                new ItemListener() {

                    @Override
                    public void itemStateChanged(ItemEvent e) {
                        updateZoomButtonsState();
                        if (e.getStateChange() == ItemEvent.SELECTED) {
                            svitchOnFullscreenMode();
                        } else {
                            switchOffFullscreenMode();
                        }
                        viewer.setSurfaceToHandleKbdFocus();
                    }
                });
        if (!isSeparateFrame) {
            zoomFullScreenButton.setEnabled(false);
            zoomFitButton.setEnabled(false);
        }
    }

    private void svitchOnFullscreenMode() {
        zoomFullScreenButton.setSelected(true);
        oldContainerBounds = container.getBounds();
        setButtonsBarVisible(false);
        forceResizable = true;
        final JFrame frame = (JFrame) container;
        frame.dispose();
        frame.setUndecorated(true);
        frame.setResizable(false);
        frame.setVisible(true);
        try {
            frame.getGraphicsConfiguration().getDevice().setFullScreenWindow(frame);
            isFullScreen = true;
            scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
            scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
            oldScrollerBorder = scroller.getBorder();
            scroller.setBorder(new EmptyBorder(0, 0, 0, 0));
            new FullscreenBorderDetectionThread(frame).start();
        } catch (Exception ex) {
            // nop
        }
    }

    private void switchOffFullscreenMode() {
        if (isFullScreen) {
            zoomFullScreenButton.setSelected(false);
            isFullScreen = false;
            setButtonsBarVisible(true);
            JFrame frame = (JFrame) container;
            try {
                frame.dispose();
                frame.setUndecorated(false);
                frame.setResizable(true);
                frame.getGraphicsConfiguration().getDevice().setFullScreenWindow(null);
            } catch (Exception e) {
                // nop
            }
            scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
            scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
            scroller.setBorder(oldScrollerBorder);
            container.setBounds(oldContainerBounds);
            frame.setVisible(true);
            pack();
        }
    }

    private void zoomToFit() {
        Dimension scrollerSize = scroller.getSize();
        Insets scrollerInsets = scroller.getInsets();
        viewer.getUiSettings().zoomToFit(scrollerSize.width - scrollerInsets.left - scrollerInsets.right,
                scrollerSize.height - scrollerInsets.top - scrollerInsets.bottom
                + (isFullScreen ? buttonsBar.getHeight() : 0),
                viewer.getWorkingProtocol().getFbWidth(), viewer.getWorkingProtocol().getFbHeight());
    }

    void registerResizeListener(Container container) {
        container.addComponentListener(new ComponentAdapter() {

            @Override
            public void componentResized(ComponentEvent e) {
                if (viewer.isZoomToFitSelected()) {
                    zoomToFit();
                    updateZoomButtonsState();
                    viewer.updateFrameTitle();
                    viewer.setSurfaceToHandleKbdFocus();
                }
            }
        });
    }

    void updateZoomButtonsState() {
        zoomOutButton.setEnabled(viewer.getUiSettings().getScalePercent() > UiSettings.MIN_SCALE_PERCENT);
        zoomInButton.setEnabled(viewer.getUiSettings().getScalePercent() < UiSettings.MAX_SCALE_PERCENT);
        zoomAsIsButton.setEnabled(viewer.getUiSettings().getScalePercent() != 100);
    }

    public ButtonsBar createButtonsBar() {
        buttonsBar = new ButtonsBar();
        return buttonsBar;
    }

    public void setButtonsBarVisible(boolean isVisible) {
        buttonsBar.setVisible(isVisible);
        if (isVisible) {
            container.add(buttonsBar.bar, BorderLayout.NORTH);
        } else {
            container.remove(buttonsBar.bar);
        }
    }

    public void setButtonsBarVisibleFS(boolean isVisible) {
        if (isVisible) {
            if (!buttonsBar.isVisible) {
                lpane.add(buttonsBar.bar, JLayeredPane.POPUP_LAYER, 0);
                final int bbWidth = buttonsBar.bar.getPreferredSize().width;
                buttonsBar.bar.setBounds(
                        scroller.getViewport().getViewPosition().x + (scroller.getWidth() - bbWidth) / 2, 0,
                        bbWidth, buttonsBar.bar.getPreferredSize().height);

                // prevent mouse events to through down to Surface
                if (null == buttonsBarMouseAdapter) {
                    buttonsBarMouseAdapter = new EmptyButtonsBarMouseAdapter();
                }
                buttonsBar.bar.addMouseListener(buttonsBarMouseAdapter);
            }
        } else {
            buttonsBar.bar.removeMouseListener(buttonsBarMouseAdapter);
            lpane.remove(buttonsBar.bar);
            lpane.repaint(buttonsBar.bar.getBounds());
        }
        buttonsBar.setVisible(isVisible);
    }

    public static class ButtonsBar {

        private static final Insets BUTTONS_MARGIN = new Insets(2, 2, 2, 2);
        private JPanel bar;
        private boolean isVisible;

        public ButtonsBar() {
            bar = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 1));
        }

        public JButton createButton(String iconId, String tooltipText, ActionListener actionListener) {
            JButton button = new JButton(Utils.getButtonIcon(iconId));
            button.setToolTipText(tooltipText);
            button.setMargin(BUTTONS_MARGIN);
            bar.add(button);
            button.addActionListener(actionListener);
            return button;
        }

        public void createStrut() {
            bar.add(Box.createHorizontalStrut(10));
        }

        public JToggleButton createToggleButton(String iconId, String tooltipText, ItemListener itemListener) {
            JToggleButton button = new JToggleButton(Utils.getButtonIcon(iconId));
            button.setToolTipText(tooltipText);
            button.setMargin(BUTTONS_MARGIN);
            bar.add(button);
            button.addItemListener(itemListener);
            return button;
        }

        public void setVisible(boolean isVisible) {
            this.isVisible = isVisible;
        }

        public int getWidth() {
            return bar.getMinimumSize().width;
        }

        public int getHeight() {
            return bar.getMinimumSize().height;
        }
    }

    private static class EmptyButtonsBarMouseAdapter extends MouseAdapter {
        // empty
    }

    private class FullscreenBorderDetectionThread extends Thread {

        public static final int SHOW_HIDE_BUTTONS_BAR_DELAY_IN_MILLS = 700;
        private final JFrame frame;
        private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        ;
		private ScheduledFuture<?> futureForShow;
        private ScheduledFuture<?> futureForHide;
        private Point mousePoint;
        private Point viewPosition;

        public FullscreenBorderDetectionThread(JFrame frame) {
            super("FS border detector");
            this.frame = frame;
        }

        public void run() {
            setPriority(Thread.MIN_PRIORITY);
            while (isFullScreen) {
                mousePoint = MouseInfo.getPointerInfo().getLocation();
                SwingUtilities.convertPointFromScreen(mousePoint, frame);
                viewPosition = scroller.getViewport().getViewPosition();
                boolean needScrolling = false;
                processButtonsBarVisibility();

                needScrolling = processVScroll() || processHScroll();
                if (needScrolling) {
                    cancelShowExecutor();
                    setButtonsBarVisibleFS(false);
                    makeScrolling(viewPosition);
                }
                try {
                    Thread.sleep(100);
                } catch (Exception e) {
                    // nop
                }
            }
        }

        private boolean processHScroll() {
            if (mousePoint.x < FS_SCROLLING_ACTIVE_BORDER) {
                if (viewPosition.x > 0) {
                    viewPosition.x -= FS_SCROLLING_ACTIVE_BORDER - mousePoint.x;
                    return true;
                }
            } else if (mousePoint.x > (frame.getWidth() - FS_SCROLLING_ACTIVE_BORDER)) {
                final Rectangle viewRect = scroller.getViewport().getViewRect();
                final int right = viewRect.width + viewRect.x;
                if (right < outerPanel.getSize().width) {
                    viewPosition.x += FS_SCROLLING_ACTIVE_BORDER - (frame.getWidth() - mousePoint.x);
                    return true;
                }
            }
            return false;
        }

        private boolean processVScroll() {
            if (mousePoint.y < FS_SCROLLING_ACTIVE_BORDER) {
                if (viewPosition.y > 0) {
                    viewPosition.y -= FS_SCROLLING_ACTIVE_BORDER - mousePoint.y;
                    return true;
                }
            } else if (mousePoint.y > (frame.getHeight() - FS_SCROLLING_ACTIVE_BORDER)) {
                final Rectangle viewRect = scroller.getViewport().getViewRect();
                final int bottom = viewRect.height + viewRect.y;
                if (bottom < outerPanel.getSize().height) {
                    viewPosition.y += FS_SCROLLING_ACTIVE_BORDER - (frame.getHeight() - mousePoint.y);
                    return true;
                }
            }
            return false;
        }

        private void processButtonsBarVisibility() {
            if (mousePoint.y < 1) {
                cancelHideExecutor();
                // show buttons bar after delay
                if (!buttonsBar.isVisible && (null == futureForShow || futureForShow.isDone())) {
                    futureForShow = scheduler.schedule(new Runnable() {

                        @Override
                        public void run() {
                            showButtonsBar();
                        }
                    }, SHOW_HIDE_BUTTONS_BAR_DELAY_IN_MILLS, TimeUnit.MILLISECONDS);
                }
            } else {
                cancelShowExecutor();
            }
            if (buttonsBar.isVisible && mousePoint.y <= buttonsBar.getHeight()) {
                cancelHideExecutor();
            }
            if (buttonsBar.isVisible && mousePoint.y > buttonsBar.getHeight()) {
                // hide buttons bar after delay
                if (null == futureForHide || futureForHide.isDone()) {
                    futureForHide = scheduler.schedule(new Runnable() {

                        @Override
                        public void run() {
                            SwingUtilities.invokeLater(new Runnable() {

                                @Override
                                public void run() {
                                    setButtonsBarVisibleFS(false);
                                    container.validate();
                                }
                            });
                        }
                    }, SHOW_HIDE_BUTTONS_BAR_DELAY_IN_MILLS, TimeUnit.MILLISECONDS);
                }
            }
        }

        private void cancelHideExecutor() {
            cancelExecutor(futureForHide);
        }

        private void cancelShowExecutor() {
            cancelExecutor(futureForShow);
        }

        private void cancelExecutor(ScheduledFuture<?> future) {
            if (future != null && !future.isDone()) {
                future.cancel(true);
            }
        }

        private void makeScrolling(final Point viewPosition) {
            SwingUtilities.invokeLater(new Runnable() {

                @Override
                public void run() {
                    scroller.getViewport().setViewPosition(viewPosition);
                    final Point mousePosition = surface.getMousePosition();
                    if (mousePosition != null) {
                        final MouseEvent mouseEvent = new MouseEvent(frame, 0, 0, 0,
                                mousePosition.x, mousePosition.y, 0, false);
                        for (MouseMotionListener mml : surface.getMouseMotionListeners()) {
                            mml.mouseMoved(mouseEvent);
                        }
                    }
                }
            });
        }

        private void showButtonsBar() {
            SwingUtilities.invokeLater(new Runnable() {

                @Override
                public void run() {
                    setButtonsBarVisibleFS(true);
                }
            });
        }
    }
}